TOMEE-4652 - roll back UserTransaction left over by a request - #2849
TOMEE-4652 - roll back UserTransaction left over by a request#2849jungm wants to merge 2 commits into
Conversation
A servlet or JSP that leaves a bean managed UserTransaction incomplete leaks that transaction to the next request served on the same pooled Tomcat exec thread. Geronimo's TransactionManagerImpl keeps the thread-to-transaction association (and the per-thread transaction timeout) in ThreadLocals that are only cleared on commit()/rollback(). EJBs are wrapped by container interceptors that restore the thread state; plain servlets have no equivalent, and OpenEJBValve's finally block cleaned up only the security context. Add TransactionCleanup, invoked from the request teardown finally in OpenEJBValve (sync path) and OpenEJBSecurityListener.asyncExit() (async complete/error/timeout). It rolls back and unassociates any dangling transaction and resets the per-thread transaction timeout, which leaks the same way. Also make CoreUserTransaction.resetError(null) remove() the ThreadLocal instead of set(null) so pooled threads don't keep an empty entry pinned. Adds UserTransactionLeakTest, which forces two sequential requests onto one exec thread (maxThreads=1) and asserts the second sees no leaked transaction.
|
Verified the premise against the Geronimo 4.0.0 sources — The placement rationale is wrong, though, and it should be corrected in the javadoc
Smaller:
Merge precondition rather than a code comment: please re-run the Jakarta Transactions TCK |
Review feedback on the placement, which was both documented backwards and covering less than it claimed. StandardContextValve has no fireRequest* call at all; StandardHostValve fires requestInitEvent, invokes the Context pipeline, and only then runs throwable()/status() and fires requestDestroyEvent. So cleaning up from OpenEJBValve (Context pipeline) ran *before* requestDestroyed rather than after it, pre-empting applications that complete their transaction there, and left <error-page> servlets and JSPs leaking exactly as before since they are dispatched through an include rather than a Pipeline. Move the call to OpenEJBSecurityListener.RequestCapturer, which is on the Host pipeline and wraps all of StandardHostValve#invoke, and correct the javadoc to describe the real ordering. This also drops the call from OpenEJBValve entirely, so the reported "skipped when listener.exit() throws" hole goes away with it. Also from review: - don't log an ERROR when the leftover transaction was already rolled back or is rolling back; unassociate it at debug instead - explain in resetTimeout() why pinning the timeout ThreadLocal is the right tradeoff here even though resetError() avoids pinning: the timeout cannot be read back, and a null value cannot pin a classloader - actually assert the timeout does not leak. The Leaker set a 120s timeout with a comment saying it must not leak and nothing checked it. Committer now reads the effective timeout of its own transaction and the test distinguishes the leaked 120s from the 600s default. Verified this assertion fails when the cleanup is removed.
|
Thanks — the ordering correction was right, and I verified it independently before acting on it ( Rather than only fix the javadoc, I moved the cleanup to Also addressed:
On the merge precondition: I have not run the Jakarta Transactions TCK, so the exclusions in apache/tomee-tck are untouched and this shouldn't merge on my testing alone. 🤖 Addressed by Claude Code |
|
Please run a CI full build for this. |
TOMEE-4652
A servlet or JSP that leaves a bean-managed
UserTransactionincomplete leaks that transaction to the next request served on the same pooled Tomcat exec thread. The victim request then sees a bogus transaction status — either missing an expectedIllegalStateExceptionor getting aNotSupportedException: Nested Transactions are not supportedon its ownbegin(). Which tests fail depends on which request lands on which thread, which is why the Transactions 2.0 TCK web vehicles (servlet + JSP) fail non-deterministically.Root cause
Geronimo's
TransactionManagerImplkeeps the thread-to-transaction association (and the per-thread transaction timeout) inThreadLocals that are only cleared bycommit()/rollback(). EJBs are wrapped by container interceptors that restore the thread state at the end of the call; plain servlets have no equivalent, andOpenEJBValve's request-teardownfinallyblock cleaned up only the security context. Since Tomcat pools its worker threads, the association survives into the next request.Fix
TransactionCleanup(new) — rolls back and unassociates any transaction still active on the thread at request end, and resets the per-thread transaction timeout (which leaks the same way, since Geronimo only clears it insidebegin()). If the rollback itself fails it falls back tosuspend()so the association never survives the request.finallyinOpenEJBValve(sync path) andOpenEJBSecurityListener.asyncExit()(async complete/error/timeout).CoreUserTransaction.resetError(null)nowremove()s theERRORThreadLocal instead ofset(null), so pooled threads don't keep an empty entry pinned. Separate hygiene issue, not the TCK cause.Testing
UserTransactionLeakTestforces two sequential requests onto a single exec thread (maxThreads=1) and asserts both actually shared the thread (so it can't pass vacuously), that the second request seesSTATUS_NO_TRANSACTION, and that it can still run a transaction of its own.Verified red/green: with the cleanup call removed the test fails with
expected:<[STATUS_NO_TRANSACTION]> but was:<[leaked status 0]>and a follow-upNotSupportedException: Nested Transactions are not supported— matching the issue exactly; with the fix it passes.tomee-catalinaandtomee-embeddedsuites are green.Notes for reviewers
runner-standalone/exclusions/transactions.txtin theapache/tomee-tckharness and rerun the 49-test baseline.maininStatefulBeanManagedTest,InterfaceTransactionTest, andTransactionPropagationTest(confirmed identical on a clean checkout).🤖 Generated with Claude Code